"""
此模块基于DataSource对象生成的映射文件，进行关联查询。
用法：
    rq = RelationQuery()
    # 对一组身份证号数据，在数据库中获取相关的关联列名，并将结果以Excel的方式输出
    rq.outUnionColumnsByIds(test_data['身份证号码'])
    # 从Excel表中读取使用的列名
    rq.inUnionColumnsByIds()
    # 形成与test_data['身份证号码']关联且包含指定列名的数据
    data = rq.queryDataByColumns()

    # 单独身份证号关联查询，在数据库中查询与指定身份证号相关联的所有数据
    rq.queryDetailInfoById("身份证号")

"""

import os
import os.path
import pandas as pd
from IPython.display import display
from datatoolsobj import id_format, excel_out_file
from tqdm import tqdm

class RelationQuery():
    """数据关联查询。
    对由DataSource产生的数据文件进行关联查询。
    """
    def __init__(self, dir_path="../标准数据", out_dir="./",datamap="datamap"):
        self.dir_path = dir_path
        self.datamap = datamap
        self.out_dir = out_dir
        # 生成文件目录
        self.datamap_dir = os.path.join(dir_path, datamap)
        # 数据文件映射表
        self.file_map_name = "数据文件映射表.xlsx"
        self.data_map_df = pd.read_excel(os.path.join(self.datamap_dir, self.file_map_name), header=1)
        # 文件标识与文件列名映射表
        self.col_map_name = "文件列名映射表.feather"
        self.col_map_df = pd.read_feather(str(os.path.join(self.datamap_dir, self.col_map_name)))
        # 生成身份证号码与文件标识映射表
        self.id_map_name = "身份证号码与文件标识映射表.feather"
        self.id_map_df = pd.read_feather(str(os.path.join(self.datamap_dir, self.id_map_name)))

        # 关联列名集合
        self.union_columns = None
        # 指定的列名
        self.use_columns = None

    def queryDataByColumns(self, use_columns=None, use_all_data=False, with_year=True):
        """根据指定的列名在全局（指定范围）内查找关联数据文件中的数据
        use_columns:指定的列名列表，如果为空，则判断self.use_columns是否为空，若非空则使用self.use_columns,否则返回空DataFrame
        use_all_data:若为True，则使用全部数据库中的列名信息进行关联，若为False则在self.union_columns范围内查找关联数据文件中的数据。
        with_year:在连接合并数据时，是否加入‘年度’列名。默认使用['身份证号码','年度']来进行外连接。
        """
        if not use_columns:
            if self.use_columns:
                use_columns = self.use_columns
            else:
                return pd.DataFrame()

        used_file_id_list = self.getFileIdByColumns(use_columns, use_all_data)
        result_df = pd.DataFrame()
        last_file_id = ""
        # 通过文件唯一标识符遍历所有关联文件
        for file_id in used_file_id_list:
            file_info = self.getFileInfo(file_id)
            data_file_info = file_info['数据文件']
            data_path = data_file_info['文件完整路径']
            unique_id_label = data_file_info['整合标识列名']
            columns_list = file_info['列名列表']
            find_columns = set(columns_list) & set(use_columns)
            if not find_columns:
                # 指定的列名列表中的所有列都不在union_columns所表示的列名范围内
                return pd.DataFrame()
            # 获取当前数据文件中，与指定列匹配的列数据内容
            find_columns = list(find_columns)
            # 由于需要使用身份证号进行合并连接，需要加入身份证号列
            if unique_id_label not in find_columns:
                find_columns.insert(0, unique_id_label)
            # 如果使用年度作为数据连接列，则需要加入
            if with_year and '年度' not in find_columns:
                find_columns.insert(1, '年度')
            data = pd.read_feather(data_path,columns=find_columns)
            # 只获取身份证号列合所有指定的列
            use_data = data[find_columns]
            # 为了方便与其他数据按照身份证号进行合并，统一数据整合唯一标识为身份证号码
            for i, item in enumerate(find_columns):
                if item == unique_id_label:
                    find_columns[i] = '身份证号码'
                    use_data.columns = find_columns
                    break

            if use_data.empty:
                # 此处应当运行不到,至少身份证号列存在
                raise ValueError(f"运行错误！在数据文件【{file_id}】中，无身份证号信息！")

            if result_df.empty:
                # 首次进行数据合并，直接赋值
                result_df = use_data
                last_file_id = "_" + str(file_id)
                continue

            # 为一提高数据合并的准确性，由于在大部分数据中都有‘年度’列名，故使用【‘年度’，‘身份证号码’】来进行连接
            # 对于不能明确年度的数据，只能设置with_year=False，但此时由于连接方式是outer，则会产生多个对应关系
            if with_year:
                on_columns = ['年度', '身份证号码']
            else:
                on_columns = ['身份证号码']

            # 将所有数据进行连接，重复的列名使用所属文件id做为区分标志
            result_df = pd.merge(left=result_df, right=use_data, how='outer', on=on_columns,
                                 suffixes=(last_file_id, "_" + str(file_id)))
            # 记录此次标志，以备下次列名重复时使用
            last_file_id = "_" + str(file_id)
        return result_df.drop_duplicates()

    def showUnionColumnsByIds(self):
        """显示给定身份证集合关联的数据文件的所有列名"""
        # union_columns = self.getUnionColumnsByIds(ids)
        show_columns = self.union_columns['涉及列名集合']
        print(show_columns)

    def inUnionColumnsByIds(self):
        """获取由outUnionColumnsByIds分析得出的列名情况输出为Excel后手工配置使用列名情况的信息
        返回指定的感兴趣的列名。
        """
        columns_data = pd.read_excel(os.path.join(self.out_dir,"身份证号关联列名设置.xlsx"),sheet_name="关联列名",header=1)
        used_columns = columns_data.loc[columns_data['是否使用']=='是',['列名']].values
        self.use_columns = [item[0] for item in used_columns]
        return self.use_columns

    def outUnionColumnsByIds(self, ids):
        """获取给定身份证集合关联的数据文件中所有列名、关联文件编号、及无相关信息的身份证号集合。将结果写入Excel文件中。
        ids:身份证列表或兼容类型的一组身份证号
        """
        self.getUnionColumnsByIds(ids)
        file_id_set = self.union_columns['涉及文件标识']
        #result_columns = union_columns['涉及列名集合']
        not_found_id_set = self.union_columns['无相关信息ID']

        #创建用于输出Excel表DataFrame数据
        columns_data = pd.DataFrame(columns=['数据文件名称','列名','数据文件编号','是否使用','标签'])
        not_found_id_data = pd.DataFrame(columns=['身份证号码'])

        # 遍历每个file_id，寻找涉及的列名，然后写入columns_data中输出
        columns_data_index = 0
        for file_id in file_id_set:
            file_info = self.getFileInfo(file_id)
            #data_file_path = file_info['数据文件']['文件完整路径']
            #unique_id_label = file_info['数据文件']['整合标识列名']
            file_name = file_info['数据文件']['文件名称']
            # 获取关联文件列名
            columns = file_info['列名列表']
            for column in columns:
                columns_data.loc[columns_data_index,:] = [file_name,column,file_id,"否",""]
                columns_data_index += 1
        # 对列名数据按照列名进行排序，方便进行横向比对
        columns_data = columns_data.sort_values(by=['列名'])

        # 对未发现信息的身份证号生成DataFrame
        not_found_id_data_index = 0
        for not_found_id in not_found_id_set:
            not_found_id_data.loc[not_found_id_data_index,:] = [not_found_id]
            not_found_id_data_index += 1

        if not columns_data.empty:
            excel_out_file(columns_data,os.path.join(self.out_dir,"身份证号关联列名设置.xlsx"),"身份证号关联列名使用情况","关联列名")
        if not not_found_id_data.empty:
            excel_out_file(not_found_id_data, os.path.join(self.out_dir, "无身份证号关联情况.xlsx"),
                       "无相关信息身份证号码", "无信息身份证号")


    def getUnionColumnsByIds(self, ids):
        """获取给定身份证集合关联数据文件中所有列名、关联文件编号 的集合
        会生成self.union_columns供其他方法使用时，确定查找范围，以便只在与此处参数给定的ids关联的文件中进行查找。
        ids:身份证列表或兼容类型的一组身份证号
        """
        # 设置列集合及对应文件集合
        columns_set = set()  # 列名集合
        file_id_set = set()  # 文件集合
        not_found_id_set = set()  # 未发现身份证号信息集合

        # 遍历所有身份证号
        for id in tqdm(ids):
            # 获取对应身份证号的详细信息，方法中只操作数据文件映射表、身份证号与文件标识映射表、文件列名映射表
            # 不涉及具体数据文件的读取，故运行速度快
            detail_info = self.getDetailInfoById(id)
            if detail_info is None:
                # print(f"身份证号{id}无相关信息！")
                not_found_id_set.add(id)
                continue
            # 遍历所有关联文件
            for file_id in detail_info:
                if file_id == '身份证号':
                    continue
                # 记录关联文件唯一标识
                file_id_set.add(file_id)
                file_data = detail_info[file_id]
                # 获取关联文件列名
                columns = file_data['列名列表']
                columns_set = columns_set.union(set(columns))
        # 将合并后的列名进行排序，便于比较
        result_columns = list(columns_set)
        result_columns.sort()
        self.union_columns = {"涉及文件标识": list(file_id_set), "涉及列名集合": result_columns,
                "无相关信息ID": list(not_found_id_set)}
        return self.union_columns

    def getFileIdByColumns(self, use_columns, use_all_data=None):
        """根据指定的列名在全局（指定范围）内查找对应文件唯一标识
        使用此方法时需要注意，如果指定的列名越通用（如年度），文件列表的范围就越大，信息就越模糊，应当避免。
        use_columns:包含的列名列表
        use_all_data:若指定为True，则在全局数据库中查找包含use_colums中列名的所有文件；若为False，则
            使用getUnionColumnsByIds调用时生成的self.union_columns查找包含列名的文件。
        """
        if self.union_columns is None or use_all_data:
            file_id_range = self.data_map_df['数据文件唯一标识'].values.tolist()
        else:
            file_id_range = self.union_columns['涉及文件标识']
        use_file_id_list = []
        for file_id in file_id_range:
            columns = self.col_map_df.loc[self.col_map_df['数据文件唯一标识'] == file_id, ['列名']].values.tolist()
            real_columns = [item[0] for item in columns]
            include_col = set(use_columns) & set(real_columns)
            if include_col:
                use_file_id_list.append(file_id)
        return use_file_id_list

    def queryDetailInfoById(self, idcard):
        """查询指定身份证号关联的数据表信息，并将其显示出来。"""
        detail_info = self.getDetailInfoById(idcard)
        if detail_info is None:
            print(f"身份证号{idcard}无相关信息！")
            return None
        self.showDetailInfo(detail_info)

    def getDetailInfoById(self, idcard):
        """根据身份证号获取详细数据信息"""
        # 规范要查询的身份证号格式
        idcard = id_format(idcard)
        if idcard is None:
            return None
        # 在身份证号码与文件标识映射表中查找所有与指定身份证号相关的文件标识
        find_file_id = self.id_map_df.loc[self.id_map_df['身份证号码'] == idcard]
        if find_file_id.empty:
            return None

        # 根据查出的文件标识，遍历所有包含指定身份证号的文件，获取对应数据文件信息
        detail_info = {}
        for index, row in find_file_id.iterrows():
            file_id = row['数据文件唯一标识']
            file_info = self.getFileInfo(file_id)
            detail_info[file_id] = file_info
        # 在详细信息中包含身份证号信息
        detail_info['身份证号'] = idcard
        return detail_info

    def showDetailInfo(self, detail_info):
        """将getDetailInfoById获得的文件详细信息输出在Jupyter中以表格形式显示
        detail_info:由getDetailInfoById查询身份证号对应的结果。
        """
        for file_id in detail_info.keys():
            if file_id == '身份证号':
                continue
            file_data = detail_info[file_id]
            data_file_path = file_data['数据文件']['文件完整路径']
            unique_id_label = file_data['数据文件']['整合标识列名']
            file_name = file_data['数据文件']['文件名称']
            data = pd.read_feather(data_file_path)
            print(file_name)
            data_by_id = data.loc[data[unique_id_label] == detail_info['身份证号']]
            display(data_by_id)

    def getFileInfo(self, file_unique_id):
        """根据文件唯一标识在数据文件映射表中获取文件信息。
        file_unique_id:文件唯一标识
        """
        # 文件标识应当是整型，进行数据转换
        file_unique_id = int(str(file_unique_id).strip())
        # 确保要查询的文件标识是有效的
        if file_unique_id not in self.data_map_df['数据文件唯一标识'].values:
            raise KeyError(f'当前数据库中不存在文件标识为【{file_unique_id}】的文件！')
        # 根据文件标识在文件列名映射表中获取文件对应列名
        columns = self.col_map_df.loc[self.col_map_df['数据文件唯一标识'] == file_unique_id, ['列名']]
        columns_list = [item[0] for item in columns.values.tolist()]
        # 根据文件标识在数据文件映射表中获取数据文件名称、整合唯一标识及完整路径
        row = self.data_map_df.loc[
            self.data_map_df['数据文件唯一标识'] == file_unique_id, ["数据文件名", "数据整合唯一标识",
                                                                     "数据文件完整路径"]].values.tolist()[0]
        return {"列名列表": columns_list,
                "数据文件": {"文件名称": row[0], "整合标识列名": row[1], "文件完整路径": row[2]}}

if __name__ == '__main__':
    rq = RelationQuery()
    print(rq.getFileIdByColumns(["残疾等级","补助金额","风险是否消除"]))
    test_data = pd.read_feather(r'../标准数据/马坞镇2021年至2024年改厕补助数据.feather')
    #union_columns = rq.getUnionColumnsByIds(test_data['身份证号码'])
    # rq.showUnionColumnsByIds(union_columns)
    # x = rq.queryDataByColumns(["学校名称",'入学时间','培训开始时间','培训结束时间'],union_columns)
    # rq.outUnionColumnsByIds(test_data['身份证号码'])
    # x['培训开始时间'] = x['培训开始时间'].replace("", np.NaN)
    # x['培训结束时间'] = x['培训结束时间'].replace("", np.NaN)
    # print(x.loc[x['培训开始时间'].notna()&x['培训结束时间'].notna()&x['入学时间'].notna()])
    # detail_info = rq.getDetailInfoById("62242920001015394X")
    # rq.showDetailInfo(detail_info)
    used_columns = rq.inUnionColumnsByIds()
    print(used_columns)
    x = rq.queryDataByColumns([])
    print(x)